// ScreenSaver.m
//
// This class is the brains behind the SpaceSaver login bundle.
//
// This recast of BackSpace into a ScreenSaver bundle for use when no one
// is logged in was done by Christopher_Lane@Med.Stanford.EDU, December 1993.
//
// The original BackSpace application was done by Sam Streeper of NeXT,
// with contributions from Bill Bumgarner, Lennart Lovstrand, Bruce Blumberg,
// shou-h@nexus.or.jp and others.  Many of the code comments are also Sam's.
//
// Undocumented 3.3 SpaceSaver.loginbundle details obtained via /bin/otool.
//
// You may freely copy, distribute, and reuse the code in this example.
// NeXT disclaims any warranty of any kind, expressed or  implied, as
// to its fitness for any particular use.

#import "SpaceSaver.h"
#import "SpaceView.h"
#import "psfuncts.h"

#import <sys/dir.h>
#import <appkit/appkit.h>

#ifdef KLUDGE
static NXDefaultsVector MovieShowDefaults = { // keep uninitialized MovieShowView from failing
	{"Jump", "NO"},
	{"SlideFrames", "NO"},
	{"SlidePauses", "YES"},
	{NULL}
	};

static NXDefaultsVector MultiViewDefaults = { // keep uninitialized MulitView from failing
	{"MultiLayout", "0"},
	{"MultiViews", "Space"},
	{NULL}
	};
#endif

static NXDefaultsVector BackSpaceDefaults = {
	{"ViewDirectory", "/LocalLibrary/BackSpaceViews"},
	{"viewType", "All"},
	{"priority", "4"},
	{NULL}
	};

static char *compiledViewNames[] = { "Space" }, *applicationName, *launchDir;

#define COMVIEWCOUNT (sizeof(compiledViewNames) / sizeof(*compiledViewNames))

typedef enum {MINIMUMPRIORITY = 0, BACKSPACEPRIORITY = 4, DEFAULTPRIORITY = 16, MAXIMUMPRIORITY = 31} PRIORITY;

#define streq(s, t) (strcmp(s, t) == 0)

#define MSECPERSEC (1000)
#define USECPERMSEC (1000)

static id _BSThinker, appDelegate;
id BSThinker() { return _BSThinker; }

static int eventMask, basePriority;
BOOL doesDidLockFocus;

#define BACKSPACENAME "BackSpace"

#define DEFAULTIMAGENAME "defaultImage"

#define BINARYEXTENSION "BackO"
#define BUNDLEEXTENSION "BackModule"

@implementation SpaceSaver

+ alloc
{
	static id instance; // probably not a good idea to have more than one of these...

	if (instance == nil) instance = [super alloc];

	return (self = instance);
}

- didStartScreenSaver
{
	if (screenSaverVal) return self;

	if (!doingSaver) [self createScreenSaver];

	[self setScreenSaver:YES];
#ifndef DEBUG
	PShidecursor();
	[self blackOutAllScreens];
#endif
	[self setVirtualViewIndex];

	[[spaceView fillBoundsWithBlack] display];
	[[spaceWindow display] makeKeyAndOrderFront:self];

#ifndef DEBUG
	(void) [spaceWindow addToEventMask:NX_MOUSEMOVEDMASK];
#endif
	eventMask = [spaceWindow eventMask];

	NXPing();

	if (priority != basePriority) cthread_priority(cthread_self(), priority, FALSE);

	if ([spaceView respondsTo:@selector(enteredScreenSaverMode)]) [spaceView enteredScreenSaverMode];

	return self;
}

- didStopScreenSaver
{
	if (!screenSaverVal) return self;

	[self setScreenSaver:NO];

	if ([spaceView respondsTo:@selector(willExitScreenSaverMode)]) [spaceView willExitScreenSaverMode];

	if (basePriority != priority) cthread_priority(cthread_self(), basePriority, FALSE);

	[spaceWindow orderOut:self];

	(void) [spaceWindow setEventMask:eventMask]; // loaded view may have changed event mask on us.
#ifndef DEBUG
	(void) [spaceWindow removeFromEventMask:NX_MOUSEMOVEDMASK];
	[self unBlackOutAllScreens];
	PSshowcursor();
#endif
	return self;
}

- oneStep
{
//	NXEvent dummyEvent;

	if (!screenSaverVal) return nil; // timed entry misfire

	[spaceView lockFocus]; {
		if (doesDidLockFocus) [spaceView didLockFocus];
//		do {
			[spaceView oneStep];
			[spaceWindow flushWindow];
			NXPing(); // Synchronize postscript for smoother animation
//			} while (screenSaverVal && ([NXApp peekNextEvent:NX_ALLEVENTS into:&dummyEvent waitFor:0 threshold:NX_BASETHRESHOLD] == NULL));
		} [spaceView unlockFocus];

	return self;
}

- createScreenSaver
{
	const char *string;
	char buffer[MAXPATHLEN];

	NXRegisterDefaults(BACKSPACENAME, BackSpaceDefaults);
#ifdef KLUDGE
	(void) sprintf(buffer, "%s/defaultImage.tiff", [[NXBundle bundleForClass:[self class]] directory]);
	(void) NXSetDefault(BACKSPACENAME, "imageFile", buffer);
	[self borrowDefaults:BACKSPACENAME]; // if module does NXGetDefaultValue([NXApp appName], ...
	NXRegisterDefaults(applicationName, MultiViewDefaults);
#endif
	launchDir = NXCopyStringBuffer([[NXBundle mainBundle] directory]);
	
	[NXApp getScreens:&screens count:&screenCount];

	[self getViewType];

	[self getPrioritySetting];

	[self getImageFile];

	if ((string = NXGetDefaultValue(BACKSPACENAME, "viewType")) != NULL) realViewIndex = [moduleList indexOfName:string];

	doingSaver = YES;

	return self;
}

- init // do the easy stuff, leave the rest for first time we 'start'
{

	_BSThinker = self;

	screenSaverVal = doingSaver = NO;

	backZone = NXCreateZone(vm_page_size, vm_page_size, YES);

	moduleList = [[ModuleList alloc] init];

	basePriority = priority = DEFAULTPRIORITY;

	realViewIndex = virtualViewIndex = -1;

	currentInspector = commonImageInspector = nullInspector = nil;

	appDelegate = [NXApp delegate];

	applicationName = (char *) [NXApp appName];
#ifndef DEBUG
	srandom(time(NULL));
#endif
#ifdef KLUDGE
	(void) fmod(2.0, 3.0); // force 'fmod' to not inline and let SpewView load
#endif
	return self;
}

- free
{
	[self didStopScreenSaver];

	return [super free];
}

int readstr(FILE *stream, char *s) // wish I could use scanf but %s won't read unanticipated "xxx yyy"
{
	int c, flag = FALSE;

	while ((c = getc(stream)) != EOF) {
		if (c == '"') flag = !flag;
		else if ((c == ' ' || c == '\n') && !flag) break;
		*s++ = c;
		}
	*s = '\0';

	return c;
}

- borrowDefaults:(const char *) realOwner
{ // NeXT should have defined (NXDefaultsVector *) NXReadDefaults(const char *owner)
	FILE *pipe;
	char string[MAXPATHLEN];

	(void) sprintf(string, "dread -o %s", realOwner);

	if ((pipe = popen(string, "r")) != NULL) {
		char owner[MAXNAMLEN], name[MAXNAMLEN], value[MAXPATHLEN];

		while (readstr(pipe, owner) != EOF && readstr(pipe, name) != EOF && readstr(pipe, value) != EOF) {
			if (streq(owner, realOwner) && NXGetDefaultValue(applicationName, name) == NULL) {
				(void) NXSetDefault(applicationName, name, value); // loginwindow & BackSpace overlap!
				}
#ifdef DEBUG
			else (void) fprintf(stderr, "%s %s %s\n", owner, name, value);
#endif
			}
		(void) pclose(pipe);
		}
	else return nil;

	return self;
}

- (NXZone *) backZone { return backZone; }

- (ModuleList *) moduleList { return moduleList; }

- installSpaceViewIntoWindow
{
	NXRect cvrect;
	unsigned int i, count;
	id subviews, contentView = [spaceWindow contentView];

	[contentView getBounds:&cvrect];

	subviews = [contentView subviews]; // remove old subviews, this is overkill really...
	for (i = 0, count = [subviews count]; i < count; i++) [[subviews objectAt:i] removeFromSuperview];

	(void) [contentView addSubview:spaceView];

	[contentView setAutoresizeSubviews:YES]; // don't really need to resize but some views break if you don't!

	[spaceView setAutosizing:NX_WIDTHSIZABLE | NX_HEIGHTSIZABLE];

	[spaceView sizeTo:cvrect.size.width :cvrect.size.height];

	return self;
}

- (int) backingTypeForView:aView
{
	if ([aView respondsTo:@selector(useBufferedWindow)] && [aView useBufferedWindow]) return NX_BUFFERED;

	return NX_RETAINED;
}

- createBigWindowIfNecessaryForBacking:(int) backing
{
	id window = nil;
#ifndef DEBUG
	const NXScreen *mainScreen = [NXApp mainScreen];
	NXRect r = mainScreen->screenBounds;
#else
	NXRect r = {{0, 0}, {640, 480}};
#endif
	if ((backing == NX_RETAINED) && !bigUnbufferedWindow) {
		window = bigUnbufferedWindow = [[Window allocFromZone:backZone]
			initContent:&r style:NX_TOKENSTYLE backing:NX_NONRETAINED buttonMask:0 defer:NO];
		}

	if ((backing == NX_BUFFERED) && !bigBufferedWindow) {
		window = bigBufferedWindow = [[Window allocFromZone:backZone]
			initContent:&r style:NX_TOKENSTYLE backing:NX_BUFFERED buttonMask:0 defer:NO];
		[[bigBufferedWindow setOneShot:YES] setDynamicDepthLimit:YES]; // want window depth to match device!
		}

	if (window != nil) {
		[[window useOptimizedDrawing:YES] setBackgroundGray:NX_BLACK];
		tweakWindow([window windowNum], SAVERTIER);
		}

	return self;
}

- setScreenSaver:(BOOL) val
{
	screenSaverVal = val;

	// I don't handle any app* messages but some modules assume I do,

	if (screenSaverVal) [NXApp setDelegate:self];
	else [NXApp setDelegate:appDelegate];

	return self;
}

- getPrioritySetting
{
	struct thread_basic_info info;
	int value, count = THREAD_BASIC_INFO_COUNT;
	const char *string = NXGetDefaultValue(BACKSPACENAME, "priority");

	if(thread_info(cthread_thread(cthread_self()), THREAD_BASIC_INFO, (thread_info_t) &info, &count) == KERN_SUCCESS) {
		basePriority = info.base_priority;
		}

	if (sscanf(string, "%d", &value) == 1 && value >= MINIMUMPRIORITY && value <= MAXIMUMPRIORITY) priority = value;
	else priority = BACKSPACEPRIORITY;

	return self;
}

BStimeval currentTimeInMs()
{
	struct timeval curTime;

	gettimeofday(&curTime, NULL);

	return ((curTime.tv_sec * MSECPERSEC) + (curTime.tv_usec / USECPERMSEC));
}

// Additional methods to handle a common image object for views.
// Lennart Lovstrad, Rank Xerox EuroPARC, August 1991.

- setImageFromName:(const char *) name
{
	if (image != nil) [image free];

	image = [[NXImage allocFromZone:backZone] initFromSection:name];

	return [self commonImageInit];
}

- setImageFromFile:(const char *) filename
{
	if (image != nil) [image free];

	image = [[NXImage allocFromZone:backZone] initFromFile:filename];

	return [self commonImageInit];
}

- commonImageInit
{
	if ([spaceView respondsTo:@selector(setImage:)]) [spaceView setImage:image];

	if ([self backingTypeForView:spaceView] != NX_BUFFERED) [[spaceView fillBoundsWithBlack] display];

	return self;
}

- getImageFile
{
	const char *filename;

	if ((filename = NXGetDefaultValue(BACKSPACENAME, "imageFile")) != NULL) [self setImageFromFile:filename];
	else [self setImageFromName:DEFAULTIMAGENAME];

	return self;
}

float frandom() // This should return a float between 0 and 1
{
	return (((float) (random() & MAXLONG)) / (float) MAXLONG);
}

float randBetween(float low, float high)
{
	float temp = low;

	if (low > high) { low = high; high = temp; }

	return (((high - low) * frandom()) + low);
}

// float randBetween(float a, float b) { return (MIN(a, b) + (fabs(a - b) * frandom())); }

- getViewType // must invoke this before creating window or setting up the views
{
	unsigned int i;

	[self loadViewsFrom:NXGetDefaultValue(BACKSPACENAME, "ViewDirectory")];

	for (i = 0; i < COMVIEWCOUNT; i++) {
		[moduleList addObject:[[ModuleInfo alloc] initWithView:nil name:compiledViewNames[i] path:NULL]];
		}

	[moduleList sort];

	return self;
}

// this method is the actual view setting mechanism,
// guaranteed to get called to set the view

- setVirtualViewIndex
{
	if (realViewIndex != -1) virtualViewIndex = realViewIndex;
	else {
#ifndef DEBUG
		virtualViewIndex = random() % [moduleList count];
#else
		if (++virtualViewIndex >= [moduleList count]) virtualViewIndex = 0;
#endif
		}

	while ([self selectScreenSaverViews] == nil || ([spaceView respondsTo:@selector(isBoringScreenSaver)] && [spaceView isBoringScreenSaver])) {
		if (++virtualViewIndex >= [moduleList count]) virtualViewIndex = 0;
		}

	return self;
}

- selectScreenSaverViews
{
	int myBacking;

	if ((spaceView = [self backView]) == nil) return nil;

	myBacking = [self backingTypeForView:spaceView];

	[self createBigWindowIfNecessaryForBacking:myBacking];

	spaceWindow = (myBacking == NX_BUFFERED) ? bigBufferedWindow : bigUnbufferedWindow;
#ifdef DEBUG
	(void) fprintf(stderr, "Window type = NX_%sBUFFERED\n", (myBacking == NX_BUFFERED) ? "" : "UN");
#endif
	[self installSpaceViewIntoWindow];
#ifdef KLUDGE
	if ([spaceView respondsTo:@selector(inspector:)]) currentInspector = [spaceView inspector:self];
	else currentInspector = [self nullInspector]; // don't use inspectors, but some modules use to initialize -- sigh
#endif
	if ([spaceView respondsTo:@selector(setImage:)]) [spaceView setImage:image];
	if ([spaceView respondsTo:@selector(newWindow)]) [spaceView newWindow];
	doesDidLockFocus = [spaceView respondsTo:@selector(didLockFocus)];

	return self;
}

- backView
{
	id theView;
	ModuleInfo *info = [moduleList objectAt:virtualViewIndex];

	if ((theView = [info view]) == nil) {
		char class[MAXNAMLEN];
#ifdef DEBUG
		NXRect aFrame = {{0, 0}, {640, 480}};
#else
		const NXScreen *mainScreen = [NXApp mainScreen];
		NXRect aFrame = mainScreen->screenBounds;
#endif
		(void) sprintf(class, "%sView", [info viewName]);

		// before I loaded all classes at launch time; now classes are loaded only as
		// needed.  This idea and some of the code here is from bill bumgarner, thanx!

		if ([info path]) { // we have path but no instance, must load class
			char path[MAXPATHLEN]; char *filenames[] = {path, NULL}; // order dependency
			long result;
			struct mach_header *header;
			NXStream *stream = NULL;
#ifdef DEBUG
			char *address;
			int length, maximum;
#endif
			do {
#ifdef DEBUG
				stream = NXOpenMemory(NULL, 0, NX_WRITEONLY);
#endif
				(void) sprintf(path, "%s/%s.%s", [info path], class, BINARYEXTENSION);
				result = objc_loadModules(filenames, stream, NULL, &header, NULL);
#ifdef DEBUG
				NXFlush(stream);
				NXGetMemoryBuffer(stream, &address, &length, &maximum);
				(void) fprintf(stderr, address);
				NXCloseMemory(stream, NX_FREEBUFFER);
#endif
				// objc_loadModules succeeds with a warning if the architecture of the
				// object file is wrong, so we better check if we really got a class

				if (result == 0 && objc_getClass(class) == nil) result = -1;

				} while (result != 0 && [info useNextPath] != nil);

			[info discardAltPaths];

#ifdef DEBUG
			(void) fprintf(stderr, "Dynamic load of class: %s -- %s!\n", class, (result == 0) ? "succeeded" : "failed");
#endif
			if (result != 0) return nil; // Ugh, failed -- return failure.
			else [info setHeader:header];
			}

		theView = [objc_getClass(class) allocFromZone:backZone]; // at this point we must have a valid name for a loaded class
#ifdef KLUDGE
		if (streq(class, "MovieShowView")) { // don't prompt user for move name
			char buffer[MAXPATHLEN];
	
			NXRegisterDefaults("MovieShow", MovieShowDefaults);

			(void) sprintf(buffer, "%s/defaultImage.anim", [[NXBundle bundleForClass:[self class]] directory]);
			(void) NXSetDefault("MovieShow", "Movie", buffer);
			}
#endif
		[info setView:[theView initFrame:&aFrame]];
		}

	return theView;
}

// Dynamically load all object files found in the specified directory
// if we find a module in several places, we save the additional paths
// in case they point to modules for different architectures

- loadViewsFrom: (const char *) dirname
{
	DIR *dir;
	int index;
	struct direct *de;
	BOOL validName, filePackage;
	char *iptr, name[MAXNAMLEN], path[MAXPATHLEN];

	if ((dir = opendir(dirname)) == NULL) return nil;

	while ((de = readdir(dir)) != NULL) {
		if (de->d_name[0] == '.') continue; // Ignore '.'-files (not really necessary, I guess)

		filePackage = validName = NO;

		if (de->d_namlen > (strlen(BINARYEXTENSION) + 1) && streq(&de->d_name[de->d_namlen - (strlen(BINARYEXTENSION) + 1)], "." BINARYEXTENSION))
			validName = YES;
		else if (de->d_namlen > (strlen(BUNDLEEXTENSION) + 1) && streq(&de->d_name[de->d_namlen - (strlen(BUNDLEEXTENSION) + 1)], "." BUNDLEEXTENSION))
			validName = filePackage = YES;

		if (!validName) continue;

		if (filePackage) (void) sprintf(path, "%s/%s", dirname, de->d_name);
		else (void) strcpy(path, dirname);

		if ((iptr = rindex(strcpy(name, de->d_name), 'V')) != NULL) *iptr = '\0'; // Smash out the 'V' in "FooView.BackO"

		if ((index = [moduleList indexOfName:name]) != -1) {
			[[moduleList objectAt:index] appendPath:path];
			continue;
			}

		// I used to load the class at this time; this got horribly inefficient.
		// I now wait until I'm about to instantiate a view before doing this (thanx bbum!)

		[moduleList addObject:[[ModuleInfo alloc] initWithView:NULL name:name path:path]];
		}

	closedir(dir);

	return self;
}

- (const char *) appDirectory { return launchDir; }

- (const char *) moduleDirectory:(const char *) name
{
	int index = [moduleList indexOfName:name];

	if (index == -1) return NULL;

	return [[moduleList objectAt:index] path];
}

- (struct mach_header *) headerForModule:(const char *) name
{
	int index = [moduleList indexOfName:name];

	if (index == -1) return NULL;

	return [[moduleList objectAt:index] header];
}

// In the multi-headed case, I gotta throw a black window over all
// the screens so they don't burn in while I do animation on one.
// You'd want to black out all screen in every case if you switched
// animations on the fly to prevent the screen from possibly being
// unlocked for a moment.

// Hmm, I don't know why I didn't just put a single big non retained
// window over all screens instead...

- blackOutAllScreens
{
	id window;
	NXRect *rect;
	unsigned int i;

	if (screenCount <= 1) return self;

	if (screenList == nil) screenList = [[List alloc] initCount:screenCount];

	for (i = 0; i < screenCount; i++) {
		rect = &screens[i].screenBounds;

		window = [[Window allocFromZone:backZone] initContent:rect style:NX_TOKENSTYLE backing:NX_NONRETAINED buttonMask:0 defer:NO];

		[screenList addObject:window];

		[window setBackgroundGray:NX_BLACK];
		(void) [window addToEventMask:NX_MOUSEMOVEDMASK];
		tweakWindow([window windowNum], SAVERTIER - 1);
		[[window placeWindowAndDisplay:rect] orderFront:self];
		}

	return self;
}

- unBlackOutAllScreens
{
	if (screenCount <= 1) return self;

	[[screenList makeObjectsPerform:@selector(orderOut:) with:self] freeObjects];

	return self;
}

- nullInspector { return nullInspector; }

- commonImageInspector { return commonImageInspector; }

@end
